#!/usr/bin/env python
# Copyright (c) 2011 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""Dump I2C transactions from raw CSV record produced by Saleae Logic.

The input file format for I2C transaction data is a comma separated value
(CSV) file with a single header row.  The columns of the file that are
used by dump_i2c are in the following format (data is optional for some
events.

<time>, <event>, [data]

Additional columns after the first three and the header row are ignored.
"""

import fileinput
import optparse
import os
import re
import sys

class I2CParseError(Exception):
  """I2C parsing error exception.

  This exception is raised when the parser failes to understand the input
  provided.  It records the file name and line number for later formatting
  of an error message that includes a message that is specific to the parse
  error.
  """
  def __init__(self, message):
    """Initialize a new I2CParseError exception."""
    self.message = message

    try:
      self.file_name = fileinput.filename()
      self.line_number = fileinput.filelineno()
    except RuntimeError:
      self.file_name = '<doctest>'
      self.line_number = 0

  def __str__(self):
    """Format the exception for user consumption."""
    return '%s:%d: %s' % (self.file_name, self.line_number, self.message)


class Transition:
  """Structure describing entries in the state transition table."""
  def __init__(self, current_state, event, next_state, action):
    self.current_state = current_state
    self.event = event
    self.next_state = next_state
    self.action = action


class I2C:
  """State machine class that accumulates and prints full I2C bus transactions.

  Once a complete bus transaction is encountered it is printed.  The output of
  this class can be further processed by device specific scripts to further
  understand the transaction.

  This example shows the basic functionality of I2C.
  >>> i2c = I2C(0x20, 1)
  >>> i2c.process('0.1,Start Bit,')
  >>> i2c.process('0.2,Write Address + ACK, 0x20')
  >>> i2c.process('0.3,Data + ACK, 0x00')
  >>> i2c.process('0.4,Stop Bit,')
  0.10000000 Write 0x20 DATA 0x00 

  Here we see I2C filtering out a transaction based on the device address.
  >>> i2c = I2C(0x20, 1)
  >>> i2c.process('0.1,Start Bit,')
  >>> i2c.process('0.2,Write Address + ACK, 0xff')
  >>> i2c.process('0.3,Data + ACK, 0x00')
  >>> i2c.process('0.4,Stop Bit,')

  Here is an example of an invalid I2C transaction sequence, there can not be
  two start bits in a row.
  >>> i2c = I2C(0x20, 1)
  >>> i2c.process('0.1,Start Bit,')
  >>> i2c.process('0.1,Start Bit,')
  Traceback (most recent call last):
  ...
  I2CParseError: <doctest>:0: Unexpected event "Start Bit"

  This is an example of I2C syncing to the beginning of the first full
  transaction presented to it.
  >>> i2c = I2C(0x20, 1)
  >>> i2c.process('0.1,Stop Bit,')
  >>> i2c.process('0.1,Start Bit,')
  >>> i2c.state == i2c.STARTED
  True

  And a completely bogus value results in a ValueError when trying to convert
  the time string to a float.
  >>> i2c = I2C(0x20, 1)
  >>> i2c.process('this,is,not,valid')
  Traceback (most recent call last):
  ...
  ValueError: invalid literal for float(): this

  Or a truncated line will throw an IndexError
  >>> i2c = I2C(0x20, 1)
  >>> i2c.process('0.1')
  Traceback (most recent call last):
  ...
  IndexError: list index out of range
  """

  SYNC = 0
  IDLE = 1
  STARTED = 2
  READING = 3
  WRITING = 4
  NAK = 5

  def StartBit(self, time, data):
    """Record start time of transaction."""
    self.message += '%.8f ' % time

  def WriteAddressNAK(self, time, data):
    """Record NAK'ed address transaction for writing."""
    self.address = int(data, 16)
    self.message += 'Write %s NAK' % data

  def WriteAddressACK(self, time, data):
    """Record ACK'ed address transaction for writing."""
    self.address = int(data, 16)
    self.message += 'Write %s DATA ' % data

  def ReadAddressNAK(self, time, data):
    """Record NAK'ed address transaction for reading."""
    self.address = int(data, 16)
    self.message += 'Read  %s NAK' % data

  def ReadAddressACK(self, time, data):
    """Record ACK'ed address transaction for reading."""
    self.address = int(data, 16)
    self.message += 'Read  %s DATA ' % data

  def AddData(self, time, data):
    """Record read or written data."""
    self.message += '%s ' % data

  def ClearMessage(self, time, data):
    """Clear accumulated transaction."""
    self.message = ''

  def PrintMessage(self, time, data):
    """Print and clear accumulated transaction."""
    if self.address == self.match_address:
      print self.message

    self.message = ''

  # This state transition table records the valid I2C bus transitions that we
  # expect to see.  Any state/action pair not defined in this table is assumed
  # to be invalid and will result in an I2CParseError being raised.
  #
  # The entries in this table correspond to the current state, the event
  # parsed, the state to transition to and the function to execute on that
  # transition.  The function is passed a CSV instance, the time of the event
  # and a possibly empty data field.
  state_table = [
    # The initial section of the state transition table describes the
    # synchronization process.  For the I2C bus this means waiting for
    # the first start or repeated start bit.  We can also transition to
    # the IDLE state when we see a stop bit because the next bit has to be
    # a start bit.  If it's not we'll raise a I2CParseError exception.
    Transition(SYNC,    'Start Bit',           STARTED, StartBit),
    Transition(SYNC,    'Repeated Start Bit',  STARTED, StartBit),
    Transition(SYNC,    'Write Address + NAK', SYNC,    None),
    Transition(SYNC,    'Write Address + ACK', SYNC,    None),
    Transition(SYNC,    'Read Address + NAK',  SYNC,    None),
    Transition(SYNC,    'Read Address + ACK',  SYNC,    None),
    Transition(SYNC,    'Data + NAK',          SYNC,    None),
    Transition(SYNC,    'Data + ACK',          SYNC,    None),
    Transition(SYNC,    'Stop Bit',            IDLE,    None),

    # After syncronization is complete the rest of the table describes the
    # expected transitions.
    Transition(IDLE,    'Start Bit',           STARTED, StartBit),
    Transition(STARTED, 'Stop Bit',            IDLE,    ClearMessage),
    Transition(STARTED, 'Write Address + NAK', NAK,     WriteAddressNAK),
    Transition(STARTED, 'Write Address + ACK', WRITING, WriteAddressACK),
    Transition(STARTED, 'Read Address + NAK',  NAK,     ReadAddressNAK),
    Transition(STARTED, 'Read Address + ACK',  READING, ReadAddressACK),
    Transition(WRITING, 'Data + NAK',          NAK,     AddData),
    Transition(WRITING, 'Data + ACK',          WRITING, AddData),
    Transition(READING, 'Data + NAK',          NAK,     AddData),
    Transition(READING, 'Data + ACK',          READING, AddData),
    Transition(WRITING, 'Stop Bit',            IDLE,    PrintMessage),
    Transition(WRITING, 'Repeated Start Bit',  STARTED, PrintMessage),
    Transition(NAK,     'Stop Bit',            IDLE,    PrintMessage)]

  def __init__(self, match_address, timeout):
    """Initialize a new I2C instance.

    The I2C instance will print all transactions with a particular I2C device
    specified by it's address up until the timeout.

    Args:
      match_address: I2C device address to filter for
      timeout: Maximum time to start recording new transactions

    >>> i2c = I2C(0x20, 1)
    >>> i2c.match_address == 0x20
    True
    >>> i2c.timeout == 1
    True
    >>> i2c.state == i2c.SYNC
    True
    """
    self.state = self.SYNC
    self.address = 0x00
    self.message = ''
    self.match_address = match_address
    self.timeout = timeout

  def process(self, line):
    """Update I2C state machine from one line of the CSV file.

    The CSV file is assumed to have the format generated by the Saleae Logic
    desktop I2C recording tool.

    These examples show how process effects the internal state of I2C.
    >>> i2c = I2C(0x20, 1)

    >>> i2c.process('0.1,Start Bit,')
    >>> i2c.state == i2c.STARTED
    True
    >>> i2c.message == '0.10000000 '
    True

    >>> i2c.process('0.1,Stop Bit,')
    >>> i2c.state == i2c.IDLE
    True
    >>> i2c.message == ''
    True
    """
    values = line.split(',')

    time = float(values[0])
    detail = ' '.join(values[1].split())

    if len(values) > 2:
      data = ' '.join(values[2].split())
    else:
      data = ''

    # Once the timeout value has been reached in the input trace we ignore all
    # future events once we've returned to the IDLE state.  We return to the
    # IDLE state at the next "Stop Bit" and stay there.
    if time > self.timeout and self.state == self.IDLE:
      return

    # Search the transition table for a matching state/action pair.
    for transition in self.state_table:
      if (transition.current_state == self.state and
          transition.event == detail):
        if transition.action:
          transition.action(self, time, data)

        self.state = transition.next_state
        break
    else:
      raise I2CParseError('Unexpected event "%s"' % detail)


def main():
  parser = optparse.OptionParser(usage = 'usage: %prog [filename] [options]\n')

  parser.add_option('-a', '--address', default=0x20,
                    type='int',
                    help='I2C device address to process',
                    action='store',
                    dest='address')

  parser.add_option('-t', '--timeout', default=100,
                    type='float',
                    help='All transactions before timeout are shown',
                    action='store',
                    dest='timeout')

  options, arguments = parser.parse_args()

  input = fileinput.input(arguments)
  i2c = I2C(options.address, options.timeout)

  for line in input:
    # The first line of the file is the header row.
    if not fileinput.isfirstline():
      try:
        i2c.process(line)
      except (I2CParseError, ValueError, IndexError) as error:
        print error
        return

def Test():
  """Run any built-in tests."""
  import doctest
  assert doctest.testmod().failed == 0


if __name__ == '__main__':
  # If first argument is --test, run testing code.
  if sys.argv[1:2] == ['--test']:
    Test()
  else:
    main()
